머신러닝이란 무엇인가?
1.1 머신러닝의 정의
머신러닝(Machine Learning)은 컴퓨터가 명시적으로 프로그래밍되지 않고도 데이터를 통해 스스로 학습하고 개선되는 인공지능의 한 분야입니다. 전통적인 프로그래밍에서는 사람이 규칙을 일일이 작성하지만, 머신러닝에서는 데이터를 주면 컴퓨터가 패턴을 발견하고 규칙을 스스로 만들어냅니다.
톰 미첼(Tom Mitchell)의 고전적 정의를 빌리면, "컴퓨터 프로그램이 경험(E)으로부터 어떤 과제(T)에 대한 성능 측도(P)가 개선되면, 그 프로그램은 과제 T에 대해 경험 E로부터 학습했다고 말한다"고 합니다. 여기서 경험은 데이터, 과제는 문제(예: 이메일 스팸 분류), 성능 측도는 정확도 같은 지표를 뜻합니다.
1.2 머신러닝의 세 가지 유형
머신러닝은 학습 방식에 따라 크게 세 가지로 나뉩니다. 첫째는 지도학습(Supervised Learning)으로, 정답(라벨)이 있는 데이터로 학습합니다. "이 이메일은 스팸이다 / 아니다"처럼 정답을 알려주면서 가르치는 방식입니다. 둘째는 비지도학습(Unsupervised Learning)으로, 정답 없이 데이터의 구조나 패턴을 스스로 발견합니다. 고객 군집화, 차원 축소 등이 대표적입니다. 셋째는 강화학습(Reinforcement Learning)으로, 에이전트가 환경과 상호작용하며 보상을 최대화하는 행동을 학습합니다. 게임 AI, 로봇 제어 등에 활용됩니다.
| 유형 | 데이터 특징 | 목적 | 대표 알고리즘 |
|---|---|---|---|
| 지도학습 | 입력 + 정답(라벨) | 예측, 분류 | 선형회귀, 결정트리, SVM, 신경망 |
| 비지도학습 | 입력만 (정답 없음) | 패턴 발견, 군집화 | K-Means, PCA, DBSCAN |
| 강화학습 | 상태-행동-보상 | 최적 행동 전략 | Q-Learning, DQN, PPO |
1.3 머신러닝이 필요한 이유
데이터의 양이 폭발적으로 증가하면서, 인간이 수작업으로 규칙을 만드는 방식은 한계에 도달했습니다. 매일 수억 건의 이메일에서 스팸을 걸러내고, 수백만 건의 거래에서 사기를 탐지하고, 수천만 장의 사진에서 얼굴을 인식하는 것은 인간이 일일이 규칙을 작성해서는 불가능한 일입니다. 머신러닝은 이런 복잡한 패턴 인식 문제에서 인간의 능력을 뛰어넘는 성능을 보여줍니다.
1.4 머신러닝 워크플로 전체 그림
어떤 머신러닝 프로젝트든 기본적인 흐름은 동일합니다. 먼저 문제를 정의하고, 데이터를 수집하고, 전처리하고, 모델을 학습시키고, 평가하고, 배포하는 과정을 거칩니다.
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐
│ 문제정의 │ → │ 데이터 │ → │ 데이터 │ → │ 모델 │ → │ 모델 │ → │ 배포 │
│ │ │ 수집 │ │ 전처리 │ │ 학습 │ │ 평가 │ │ │
└─────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────┘
↑ │
└──────── 피드백 반복 ─────────┘
지도학습(Supervised Learning) 개요
2.1 지도학습이란?
지도학습은 입력 데이터(특성, Feature)와 그에 대응하는 정답(라벨, Label)이 쌍으로 주어진 데이터셋을 이용하여 입력에서 출력으로의 매핑 함수를 학습하는 방법입니다. "지도(Supervised)"라는 이름은 마치 선생님이 학생에게 문제와 정답을 함께 알려주면서 가르치는 것과 같다는 비유에서 유래합니다.
예를 들어, 부동산 가격 예측을 생각해봅시다. "면적 85㎡, 강남구, 10층 → 12억 원"과 같이 특성(면적, 위치, 층수)과 정답(가격)이 함께 주어진 수천 건의 과거 거래 데이터로 모델을 학습시키면, 학습 후에는 새로운 아파트의 특성만 입력해도 가격을 예측할 수 있습니다.
2.2 핵심 용어 정리
| 용어 | 영문 | 설명 | 비유 |
|---|---|---|---|
| 특성 (피처) | Feature | 모델에 입력하는 독립 변수 | 시험 문제의 조건 |
| 라벨 (타겟) | Label / Target | 예측하고자 하는 종속 변수 (정답) | 시험의 정답 |
| 학습 데이터 | Training Data | 모델 학습에 사용하는 데이터 | 연습 문제 |
| 테스트 데이터 | Test Data | 모델 성능 평가에 사용하는 데이터 | 본 시험 |
| 모델 | Model | 데이터에서 학습된 입력→출력 매핑 규칙 | 학습한 학생의 지식 |
| 예측 | Prediction | 학습된 모델이 새 데이터에 대해 출력한 값 | 시험 답안 |
| 손실 함수 | Loss Function | 예측과 정답의 차이를 수치화하는 함수 | 채점 기준 |
| 학습(훈련) | Training | 손실을 최소화하도록 모델 파라미터를 조정하는 과정 | 공부 과정 |
2.3 지도학습의 학습 과정
지도학습의 핵심은 "예측값과 실제 정답의 차이(오차)를 줄이는 방향으로 모델을 개선해 나가는 것"입니다. 이 과정을 좀 더 구체적으로 살펴보면 다음과 같습니다.
먼저 모델의 파라미터(가중치)를 랜덤하게 초기화합니다. 학습 데이터를 모델에 넣어 예측값을 구합니다. 손실 함수를 통해 예측값과 실제 정답의 차이(손실)를 계산합니다. 이 손실을 줄이는 방향으로 파라미터를 조금씩 업데이트합니다. 이 과정을 수천~수만 번 반복하면 점차 예측 정확도가 올라갑니다.
┌──────────────────────────────────────────────────┐ │ ① 입력 데이터(X) → 모델(f) → 예측값(ŷ) │ │ ② 손실 = Loss(ŷ, y) (ŷ: 예측, y: 정답) │ │ ③ 그래디언트 계산 (∂Loss/∂w) │ │ ④ 파라미터 업데이트: w = w - α × ∂Loss/∂w │ │ ⑤ ① ~ ④ 반복 (수렴할 때까지) │ └──────────────────────────────────────────────────┘
2.4 지도학습을 쓰는 실제 사례
지도학습은 우리 일상 곳곳에 적용되어 있습니다. 이메일 스팸 필터(스팸/정상 분류), 신용카드 사기 탐지(사기/정상 분류), 의료 영상 진단(양성/음성 분류), 주식 가격 예측(연속값 회귀), 넷플릭스 영화 평점 예측(1~5점 회귀), 자율주행 차량의 객체 인식(보행자/차량/신호등 분류), 음성 인식(음파 → 텍스트), 번역(한국어 → 영어) 등이 모두 지도학습의 대표적인 응용입니다.
회귀(Regression) vs 분류(Classification)
3.1 출력 변수의 종류가 핵심
지도학습은 예측하려는 타겟 변수의 성격에 따라 회귀(Regression)와 분류(Classification) 두 가지로 나뉩니다. 이 구분은 매우 중요한데, 어떤 유형인지에 따라 사용하는 알고리즘, 손실 함수, 평가 지표가 모두 달라지기 때문입니다.
3.2 회귀 (Regression)
회귀는 연속적인 숫자값을 예측하는 문제입니다. "이 아파트의 가격은 얼마일까?", "내일 서울의 기온은 몇 도일까?", "이 고객의 다음 달 구매 금액은 얼마일까?"처럼 결과가 무한히 세밀한 실수(Real Number)로 나오는 경우입니다. 출력값이 12.5억, 23.7°C, 158,000원 등 연속적인 수치로 표현됩니다.
3.3 분류 (Classification)
분류는 유한한 범주(카테고리) 중 하나를 예측하는 문제입니다. "이 이메일은 스팸인가 아닌가?", "이 사진 속 동물은 개인가 고양이인가?", "이 환자의 종양은 양성인가 악성인가?"처럼 결과가 정해진 클래스 중 하나로 나옵니다. 클래스가 2개이면 이진 분류(Binary Classification), 3개 이상이면 다중 분류(Multi-class Classification)라고 합니다.
| 구분 | 회귀 (Regression) | 분류 (Classification) |
|---|---|---|
| 출력 형태 | 연속적인 숫자 (실수) | 이산적인 범주 (클래스) |
| 예시 문제 | 집값 예측, 온도 예측, 매출 예측 | 스팸 탐지, 질병 진단, 이미지 인식 |
| 대표 알고리즘 | 선형 회귀, 릿지, 라쏘, SVR | 로지스틱 회귀, SVM, 결정 트리 |
| 주요 손실 함수 | MSE, MAE, RMSE | Cross-Entropy, Hinge Loss |
| 주요 평가 지표 | R², RMSE, MAE | Accuracy, Precision, Recall, F1, AUC |
3.4 경계선 위의 문제들
때때로 회귀와 분류의 경계가 모호한 문제도 있습니다. 예를 들어 영화 평점(1~5점)은 숫자이지만 이산적인 값이므로 분류로도, 회귀로도 접근할 수 있습니다. 순서가 있는 범주형 문제(Ordinal Classification)라고 부르기도 합니다. 고객 만족도(매우불만, 불만, 보통, 만족, 매우만족)도 비슷한 사례입니다. 이런 경우 두 접근법을 모두 시도해보고 성능이 좋은 쪽을 선택하는 것이 실무에서의 합리적인 방법입니다.
데이터 전처리 (Data Preprocessing)
4.1 왜 전처리가 중요한가?
실무 데이터는 완벽하지 않습니다. 결측값이 있고, 이상치가 섞여 있고, 단위와 스케일이 제각각이고, 범주형 데이터가 문자열로 되어 있습니다. "쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out)"는 격언처럼, 아무리 뛰어난 알고리즘도 질 낮은 데이터에서는 좋은 결과를 얻을 수 없습니다. 머신러닝 프로젝트에서 데이터 전처리에 전체 시간의 60~80%가 소요된다는 것은 업계의 공통된 경험입니다.
4.2 결측값 처리 (Missing Values)
결측값은 데이터가 비어 있는 경우입니다. 처리 방법으로는 해당 행이나 열을 제거하는 방법, 평균/중앙값/최빈값으로 채우는 방법, 그리고 머신러닝 모델(KNN 등)로 예측하여 채우는 방법이 있습니다. 결측값이 소수이면 삭제해도 되지만, 비율이 높으면 적절한 대체(Imputation) 전략이 필요합니다.
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer
# 결측값 확인
df.isnull().sum()
# 방법 1: 결측값이 있는 행 삭제
df_clean = df.dropna()
# 방법 2: 평균값으로 채우기
imputer = SimpleImputer(strategy='mean')
df['age'] = imputer.fit_transform(df[['age']])
# 방법 3: 중앙값으로 채우기 (이상치에 강건)
df['income'].fillna(df['income'].median(), inplace=True)
# 방법 4: 최빈값으로 채우기 (범주형 데이터)
df['city'].fillna(df['city'].mode()[0], inplace=True)
4.3 특성 스케일링 (Feature Scaling)
특성마다 값의 범위가 크게 다르면 모델 학습에 문제가 생깁니다. 예를 들어 "나이(0~100)"와 "연봉(0~2억)"이 같은 데이터셋에 있으면, 연봉의 숫자가 워낙 크기 때문에 모델이 나이의 영향을 무시할 수 있습니다. 이를 해결하기 위해 모든 특성을 비슷한 범위로 조정하는 스케일링이 필요합니다.
| 방법 | 수식 | 결과 범위 | 특징 |
|---|---|---|---|
| Min-Max 정규화 | (x - min) / (max - min) | 0 ~ 1 | 이상치에 민감, 범위가 명확 |
| 표준화 (Z-score) | (x - μ) / σ | 평균 0, 표준편차 1 | 이상치에 덜 민감, 가장 보편적 |
| Robust Scaling | (x - 중앙값) / IQR | 가변 | 이상치에 가장 강건 |
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 표준화 (가장 많이 사용)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # ★ test는 transform만!
# Min-Max 정규화
minmax = MinMaxScaler()
X_train_norm = minmax.fit_transform(X_train)
X_test_norm = minmax.transform(X_test)
4.4 범주형 인코딩 (Categorical Encoding)
대부분의 머신러닝 알고리즘은 숫자만 입력으로 받기 때문에, "서울", "부산" 같은 범주형(카테고리) 데이터를 숫자로 변환해야 합니다.
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
import pandas as pd
# 라벨 인코딩: 순서가 있는 범주에 적합
# (예: 학력 - 고졸:0, 대졸:1, 석사:2)
le = LabelEncoder()
df['education_encoded'] = le.fit_transform(df['education'])
# 원-핫 인코딩: 순서가 없는 범주에 적합
# (예: 도시 - 서울, 부산, 대전)
df_encoded = pd.get_dummies(df, columns=['city'], drop_first=True)
# 결과: city_부산, city_대전 컬럼이 0/1로 생성됨
4.5 데이터 분할 (Train / Test Split)
모델을 학습시킨 데이터로 바로 평가하면, 시험 문제를 미리 본 학생과 같아서 실제 실력을 알 수 없습니다. 따라서 전체 데이터를 학습용(Training Set)과 평가용(Test Set)으로 분리해야 합니다. 일반적으로 70~80%를 학습에, 20~30%를 테스트에 사용합니다.
from sklearn.model_selection import train_test_split
# 80% 학습, 20% 테스트
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42, # 재현 가능성을 위한 시드
stratify=y # 분류: 클래스 비율 유지
)
선형 회귀 (Linear Regression)
5.1 가장 직관적인 알고리즘
선형 회귀는 입력 특성과 출력값 사이의 선형 관계를 찾는 알고리즘으로, 모든 머신러닝의 출발점입니다. "공부 시간이 1시간 늘면 점수가 5점 오른다"와 같이 직선(또는 초평면)을 찾는 것이 핵심입니다.
ŷ: 예측값, x: 입력, w₁: 가중치(기울기), b: 편향(절편)
여러 개의 특성(x₁~xₙ)을 사용하여 예측하는 일반화된 형태
5.2 손실 함수: MSE (평균 제곱 오차)
선형 회귀의 목표는 예측값과 실제값의 차이를 최소화하는 직선을 찾는 것입니다. 이 차이를 수치화하는 것이 손실 함수이며, 선형 회귀에서는 MSE(Mean Squared Error)를 가장 많이 사용합니다.
각 데이터의 (실제값 - 예측값)²의 평균. 값이 작을수록 좋음
제곱을 하는 이유는 두 가지입니다. 첫째, 양수/음수 오차가 상쇄되는 것을 방지합니다. 둘째, 큰 오차에 더 큰 페널티를 부여하여 모델이 큰 실수를 피하도록 유도합니다.
5.3 학습 방법: 경사 하강법 (Gradient Descent)
경사 하강법은 산에서 가장 낮은 골짜기를 찾아 내려가는 것에 비유할 수 있습니다. 현재 위치에서 경사가 가장 가파르게 내려가는 방향으로 한 발씩 내딛는 것입니다. 여기서 "경사"는 손실 함수의 기울기(그래디언트)이고, "한 발의 크기"는 학습률(Learning Rate)입니다.
학습률이 너무 크면 골짜기를 건너뛰어 발산하고, 너무 작으면 도달하는 데 시간이 매우 오래 걸립니다. 적절한 학습률을 설정하는 것이 중요합니다.
5.4 코드 실습
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
# 캘리포니아 주택 가격 데이터
data = fetch_california_housing()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 모델 학습
model = LinearRegression()
model.fit(X_train, y_train)
# 예측 및 평가
y_pred = model.predict(X_test)
print(f"RMSE: {mean_squared_error(y_test, y_pred, squared=False):.4f}")
print(f"R² Score: {r2_score(y_test, y_pred):.4f}")
# 가중치(계수) 확인
for name, coef in zip(data.feature_names, model.coef_):
print(f" {name:12s}: {coef:.4f}")
5.5 선형 회귀의 한계와 확장
선형 회귀는 간단하고 해석이 쉽지만, 데이터가 비선형 관계일 때는 성능이 떨어집니다. 이를 보완하기 위해 다항 회귀(Polynomial Regression), 릿지 회귀(Ridge), 라쏘 회귀(Lasso), 엘라스틱넷(ElasticNet) 같은 확장 기법이 존재합니다. 이들은 Chapter 13에서 정규화 기법과 함께 다룹니다.
로지스틱 회귀 (Logistic Regression)
6.1 이름은 회귀, 실제는 분류
로지스틱 회귀는 이름에 "회귀"가 들어가지만, 실제로는 분류 알고리즘입니다. 선형 회귀의 출력을 시그모이드(Sigmoid) 함수에 통과시켜 0과 1 사이의 확률값으로 변환하고, 이 확률을 기반으로 클래스를 결정합니다.
z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b
σ(z) ≥ 0.5 → 클래스 1(양성) | σ(z) < 0.5 → 클래스 0(음성)
시그모이드 함수는 어떤 실수값이든 입력하면 0~1 사이로 변환해주는 S자 형태의 함수입니다. 이 덕분에 출력을 "확률"로 해석할 수 있습니다. 예를 들어 σ(z) = 0.87이면 "이 이메일이 스팸일 확률이 87%"로 해석할 수 있습니다.
6.2 손실 함수: Binary Cross-Entropy
선형 회귀에서 MSE를 사용한 것과 달리, 로지스틱 회귀는 크로스 엔트로피(Cross-Entropy)를 손실 함수로 사용합니다. 이는 확률 분포 간의 차이를 측정하는 함수입니다.
y: 실제값(0 또는 1), ŷ: 예측 확률(0~1)
6.3 코드 실습
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import accuracy_score, classification_report
# 유방암 진단 데이터 (이진 분류)
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2,
random_state=42, stratify=data.target
)
# 모델 학습
model = LogisticRegression(max_iter=10000)
model.fit(X_train, y_train)
# 예측 (클래스)
y_pred = model.predict(X_test)
# 예측 (확률) - 스팸 확률 87% 같은 값
y_prob = model.predict_proba(X_test)[:, 1]
print(f"정확도: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred,
target_names=data.target_names))
6.4 다중 클래스 확장
로지스틱 회귀는 기본적으로 이진 분류이지만, 다중 클래스로 확장할 수 있습니다. OvR(One-vs-Rest) 방식은 각 클래스에 대해 "이 클래스인가 아닌가"를 판별하는 분류기를 각각 만드는 방식이고, 소프트맥스(Softmax) 회귀는 모든 클래스에 대한 확률을 동시에 계산하는 방식입니다.
결정 트리 (Decision Tree)
7.1 스무고개 게임과 결정 트리
결정 트리는 가장 직관적인 머신러닝 알고리즘입니다. "스무고개 게임"을 떠올리면 됩니다. "체온이 37.5도 이상인가?" → "기침이 있는가?" → "접촉 이력이 있는가?"처럼 예/아니오 질문을 연속으로 던져서 최종 결론에 도달하는 방식입니다. 트리의 각 분기점(노드)이 하나의 질문이고, 맨 아래 잎(리프 노드)이 최종 예측 결과입니다.
7.2 분할 기준: 불순도(Impurity)
결정 트리의 핵심은 "어떤 질문(특성과 기준값)으로 분할할 것인가"입니다. 목표는 분할 후 각 그룹이 최대한 "순수(한 클래스만 포함)"하도록 만드는 것입니다. 이 순수도를 측정하는 대표적인 지표가 지니 불순도(Gini Impurity)와 엔트로피(Entropy)입니다.
pᵢ: 각 클래스의 비율. 값이 0이면 완전히 순수, 0.5면 최대 불순(이진 분류)
값이 0이면 완전히 순수, 1이면 최대 불확실(이진 분류)
트리는 모든 가능한 분할을 시도하고, 불순도 감소(Information Gain)가 가장 큰 분할을 선택합니다. 이 과정을 재귀적으로 반복하여 트리를 성장시킵니다.
7.3 코드 실습 및 시각화
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
# 붓꽃 데이터
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42
)
# 결정 트리 (깊이 제한으로 과적합 방지)
tree = DecisionTreeClassifier(
max_depth=3,
criterion='gini',
random_state=42
)
tree.fit(X_train, y_train)
print(f"정확도: {tree.score(X_test, y_test):.4f}")
# 트리 시각화
plt.figure(figsize=(16, 8))
plot_tree(tree, feature_names=iris.feature_names,
class_names=iris.target_names, filled=True)
plt.show()
7.4 결정 트리의 장단점
결정 트리의 가장 큰 장점은 해석 가능성(Interpretability)입니다. 왜 그런 예측을 했는지 트리 경로를 따라가면 바로 알 수 있습니다. 또한 특성 스케일링이 필요 없고, 수치형과 범주형 데이터를 모두 자연스럽게 처리합니다.
반면 가장 큰 단점은 과적합(Overfitting)에 취약하다는 점입니다. 제한 없이 트리를 성장시키면 학습 데이터에 완벽히 맞추지만 새로운 데이터에는 성능이 크게 떨어집니다. 이를 해결하기 위해 max_depth, min_samples_split, min_samples_leaf 같은 하이퍼파라미터로 가지치기(Pruning)를 합니다. 또한 결정 트리의 약점을 보완하기 위해 여러 트리를 결합하는 앙상블 기법이 발전했습니다.
앙상블 학습 (Ensemble Learning)
8.1 "세 사람의 지혜가 한 천재를 이긴다"
앙상블(Ensemble)은 여러 개의 약한 학습기(Weak Learner)를 결합하여 하나의 강한 학습기(Strong Learner)를 만드는 기법입니다. 한 명의 전문가보다 여러 전문가의 합의가 더 정확한 것처럼, 여러 모델의 예측을 종합하면 개별 모델보다 훨씬 좋은 성능을 얻을 수 있습니다.
8.2 배깅 (Bagging) - 랜덤 포레스트
배깅(Bootstrap Aggregating)은 학습 데이터에서 랜덤하게 부분 집합을 여러 번 추출(부트스트랩)하고, 각 부분 집합으로 독립적인 모델을 학습시킨 후, 모든 모델의 예측을 투표(분류) 또는 평균(회귀)으로 결합합니다.
랜덤 포레스트(Random Forest)는 배깅의 대표 알고리즘으로, 여러 결정 트리를 랜덤하게 만들어 결합합니다. 각 트리를 만들 때 데이터뿐만 아니라 특성도 랜덤하게 일부만 선택하여 트리 간의 다양성을 높입니다. 이를 통해 단일 결정 트리의 과적합 문제를 효과적으로 해결합니다.
from sklearn.ensemble import RandomForestClassifier
# 랜덤 포레스트: 100개 트리의 앙상블
rf = RandomForestClassifier(
n_estimators=100, # 트리 수
max_depth=10, # 각 트리의 최대 깊이
random_state=42,
n_jobs=-1 # 모든 CPU 코어 사용
)
rf.fit(X_train, y_train)
print(f"정확도: {rf.score(X_test, y_test):.4f}")
# 특성 중요도 확인
import pandas as pd
importance = pd.Series(rf.feature_importances_,
index=feature_names)
importance.sort_values(ascending=False).head(10).plot.barh()
plt.title('Feature Importance')
plt.show()
8.3 부스팅 (Boosting) - 이전 모델의 실수를 보완
부스팅은 배깅과 달리 모델을 순차적으로 학습시킵니다. 이전 모델이 틀린 데이터에 더 집중하여 다음 모델을 학습시키는 방식으로, 점차적으로 정확도를 향상시킵니다. 실수에서 배우는 학생처럼, 약점을 보완하며 점점 강해집니다.
| 알고리즘 | 개발 | 핵심 특징 | 장점 |
|---|---|---|---|
| AdaBoost | 1995, Freund & Schapire | 잘못 분류된 샘플에 가중치 부여 | 구현 간단, 이해 쉬움 |
| Gradient Boosting | 2001, Friedman | 잔차(Residual)를 학습 | 유연성 높음 |
| XGBoost | 2014, Tianqi Chen | 정규화 + 병렬 처리 + 결측치 처리 | Kaggle 대회 최다 우승 |
| LightGBM | 2017, Microsoft | 리프 중심 트리 성장, 히스토그램 기반 | 대용량 데이터에서 매우 빠름 |
| CatBoost | 2017, Yandex | 범주형 특성 자동 처리 | 범주형 데이터에 강함 |
# XGBoost
from xgboost import XGBClassifier
xgb = XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
random_state=42,
use_label_encoder=False,
eval_metric='logloss'
)
xgb.fit(X_train, y_train)
print(f"XGBoost 정확도: {xgb.score(X_test, y_test):.4f}")
# LightGBM
from lightgbm import LGBMClassifier
lgbm = LGBMClassifier(
n_estimators=200,
max_depth=-1, # 제한 없음
learning_rate=0.1,
random_state=42,
verbose=-1
)
lgbm.fit(X_train, y_train)
print(f"LightGBM 정확도: {lgbm.score(X_test, y_test):.4f}")
8.4 배깅 vs 부스팅 비교
| 구분 | 배깅 (Bagging) | 부스팅 (Boosting) |
|---|---|---|
| 학습 방식 | 병렬 (독립적) | 순차적 (이전 모델 의존) |
| 주요 목표 | 분산(Variance) 감소 | 편향(Bias) 감소 |
| 과적합 위험 | 낮음 | 상대적으로 높음 |
| 대표 알고리즘 | Random Forest | XGBoost, LightGBM, CatBoost |
| 속도 | 병렬 처리 가능 | 순차 처리로 상대적으로 느림 |
서포트 벡터 머신 (SVM)
9.1 최적의 결정 경계를 찾아서
서포트 벡터 머신(Support Vector Machine, SVM)은 두 클래스를 가장 잘 구분하는 결정 경계(Decision Boundary)를 찾는 알고리즘입니다. 핵심 아이디어는 "두 클래스 사이의 마진(Margin)을 최대화하는 초평면(Hyperplane)을 찾는 것"입니다.
마진이란 결정 경계와 각 클래스에서 가장 가까운 데이터 점(서포트 벡터) 사이의 거리입니다. 마진이 클수록 새로운 데이터에 대한 일반화 능력이 높다고 볼 수 있습니다. SVM은 이 마진을 최대화하는 수학적 최적화 문제를 풉니다.
9.2 커널 트릭 (Kernel Trick)
SVM의 진정한 힘은 커널 트릭에 있습니다. 현실의 데이터는 직선 하나로 깔끔하게 분리되지 않는 경우가 많습니다. 커널 트릭은 데이터를 더 높은 차원으로 매핑하여 선형으로 분리 가능하게 만드는 기법입니다. 2차원에서 동심원 형태로 분포된 데이터도, 3차원으로 올리면 평면 하나로 분리할 수 있습니다.
| 커널 | 특징 | 적합한 경우 |
|---|---|---|
| Linear (선형) | 직선/초평면으로 분리 | 특성 수가 많고 데이터가 적을 때 |
| RBF (가우시안) | 비선형, 방사형 기저 함수 | 가장 범용적, 기본 선택 |
| Polynomial (다항식) | 다항식 결정 경계 | 특정 비선형 패턴 |
| Sigmoid | 시그모이드 형태 | 신경망과 유사한 효과 |
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
# SVM은 스케일링이 필수! Pipeline으로 편리하게 처리
svm_pipeline = Pipeline([
('scaler', StandardScaler()),
('svm', SVC(
kernel='rbf', # RBF 커널
C=1.0, # 정규화 파라미터
gamma='scale', # 커널 계수
probability=True # 확률 출력 활성화
))
])
svm_pipeline.fit(X_train, y_train)
print(f"SVM 정확도: {svm_pipeline.score(X_test, y_test):.4f}")
9.3 SVM의 장단점
SVM은 고차원 데이터에서 강력한 성능을 보이며, 커널 트릭 덕분에 복잡한 비선형 관계도 잘 처리합니다. 서포트 벡터만으로 결정 경계가 결정되므로 이상치에 상대적으로 강건합니다. 하지만 대용량 데이터에서 학습 속도가 매우 느리고(O(n²)~O(n³)), 확률 추정이 기본적으로는 어렵고, 하이퍼파라미터(C, gamma) 튜닝에 민감하다는 단점이 있습니다. 데이터가 10만 건 이상이면 SVM보다 트리 기반 앙상블이나 신경망이 더 적합합니다.
K-최근접 이웃 (KNN)
10.1 "너의 이웃이 누구인지 말해봐, 네가 누구인지 알려줄게"
KNN(K-Nearest Neighbors)은 가장 직관적인 알고리즘 중 하나입니다. 새로운 데이터가 들어오면 학습 데이터 중에서 가장 가까운 K개의 이웃을 찾고, 이웃들의 다수결(분류) 또는 평균(회귀)으로 예측합니다. 새로운 동네로 이사 온 사람의 성향을 주변 이웃으로 추측하는 것과 같습니다.
KNN은 별도의 학습 과정이 없는 "게으른 학습기(Lazy Learner)"입니다. 모든 계산을 예측 시점에 수행하므로, 학습은 순식간이지만 예측이 느립니다.
10.2 K값의 선택
K는 참조할 이웃의 수입니다. K가 너무 작으면(예: K=1) 노이즈에 민감해져 과적합되고, K가 너무 크면 지나치게 평균화되어 과소적합됩니다. 일반적으로 홀수를 사용하여 동점을 방지하며, 교차 검증으로 최적의 K를 찾습니다. 보통 3~15 사이에서 좋은 결과가 나옵니다.
from sklearn.neighbors import KNeighborsClassifier
# 최적 K 탐색
best_k, best_score = 1, 0
for k in range(1, 21, 2):
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train_scaled, y_train)
score = knn.score(X_test_scaled, y_test)
if score > best_score:
best_k, best_score = k, score
print(f"K={k:2d} → 정확도: {score:.4f}")
print(f"\n최적 K={best_k}, 정확도={best_score:.4f}")
10.3 거리 측정 방법
"가까운 이웃"을 찾으려면 거리를 정의해야 합니다. 유클리드 거리(직선 거리)가 기본이지만, 맨해튼 거리(격자 거리), 민코프스키 거리, 코사인 유사도 등 다양한 거리 척도를 사용할 수 있습니다. KNN도 SVM과 마찬가지로 스케일링이 필수인데, 단위가 다른 특성이 섞이면 거리 계산이 왜곡되기 때문입니다.
나이브 베이즈 (Naive Bayes)
11.1 확률에 기반한 분류
나이브 베이즈는 베이즈 정리(Bayes' Theorem)에 기반한 확률적 분류 알고리즘입니다. "나이브(Naive, 순진한)"라는 이름은 모든 특성이 서로 독립이라는 강한 가정 때문입니다. 현실에서 이 가정이 완벽히 성립하는 경우는 거의 없지만, 놀랍게도 실전에서 꽤 좋은 성능을 보입니다.
P(y|X): 데이터 X가 주어졌을 때 클래스 y일 확률 (사후확률)
P(X|y): 클래스 y일 때 데이터 X가 나올 확률 (우도)
P(y): 클래스 y의 사전확률 | P(X): 데이터 X의 전체 확률
11.2 나이브 베이즈의 종류
데이터의 분포 가정에 따라 세 가지 변형이 있습니다. 가우시안 나이브 베이즈(GaussianNB)는 특성이 정규분포를 따른다고 가정하며 연속적인 수치 데이터에 적합합니다. 다항분포 나이브 베이즈(MultinomialNB)는 특성이 빈도수(카운트)를 나타내며 텍스트 분류에 주로 사용됩니다. 베르누이 나이브 베이즈(BernoulliNB)는 특성이 이진(0/1)값이며 문서의 단어 존재 여부 기반 분류에 적합합니다.
from sklearn.naive_bayes import GaussianNB, MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
# 1. 수치 데이터 → Gaussian NB
gnb = GaussianNB()
gnb.fit(X_train, y_train)
print(f"GaussianNB 정확도: {gnb.score(X_test, y_test):.4f}")
# 2. 텍스트 데이터 → Multinomial NB (스팸 필터 예시)
texts = ["무료 쿠폰 지금 받으세요", "내일 회의 참석 가능하세요?",
"당첨 축하합니다 상금 수령", "프로젝트 보고서 검토 부탁드립니다"]
labels = [1, 0, 1, 0] # 1: 스팸, 0: 정상
vectorizer = CountVectorizer()
X_text = vectorizer.fit_transform(texts)
mnb = MultinomialNB()
mnb.fit(X_text, labels)
# 새 이메일 분류
new_email = vectorizer.transform(["무료 상금 당첨 축하"])
print(f"스팸 확률: {mnb.predict_proba(new_email)[0][1]:.2%}")
11.3 나이브 베이즈의 특성
나이브 베이즈는 학습과 예측 모두 매우 빠르고, 적은 데이터로도 잘 작동하며, 고차원 데이터(텍스트 등)에서 특히 좋은 성능을 보입니다. 스팸 필터, 감성 분석, 문서 분류 등 텍스트 NLP 분야에서 여전히 강력한 베이스라인으로 활용됩니다. 독립 가정이 위반되면 확률값 자체는 부정확하지만, 분류 결정(가장 높은 확률의 클래스 선택)은 여전히 정확한 경우가 많다는 것이 흥미로운 점입니다.
신경망과 딥러닝 기초
12.1 인공 신경망의 영감
인공 신경망(Artificial Neural Network, ANN)은 인간 뇌의 뉴런 구조에서 영감을 받은 알고리즘입니다. 하나의 인공 뉴런(퍼셉트론)은 여러 입력을 받아 가중합을 계산하고, 활성화 함수를 통과시켜 출력을 내보냅니다. 이런 뉴런들을 여러 층(Layer)으로 쌓아 복잡한 패턴을 학습할 수 있게 한 것이 심층 신경망, 즉 딥러닝(Deep Learning)입니다.
12.2 신경망의 구조
신경망은 크게 세 부분으로 구성됩니다. 입력층(Input Layer)은 데이터의 특성을 받아들이는 층입니다. 은닉층(Hidden Layer)은 실제 패턴을 학습하는 중간 층으로, 여러 개 쌓을 수 있습니다. 출력층(Output Layer)은 최종 예측 결과를 출력하는 층입니다. 은닉층이 2개 이상이면 "심층(Deep)" 신경망이라 부르며, 이것이 딥러닝의 "딥"입니다.
각 층의 뉴런은 이전 층의 모든 뉴런과 가중치(Weight)로 연결됩니다. 입력값에 가중치를 곱하고, 편향(Bias)을 더한 뒤, 활성화 함수를 통과시켜 다음 층으로 전달합니다. 이 과정을 순전파(Forward Propagation)라고 합니다.
입력층 은닉층 1 은닉층 2 출력층
(특성들)
[x₁] ──┐
├──→ [h₁] ──┐
[x₂] ──┤ ├──→ [h₄] ──┐
├──→ [h₂] ──┤ ├──→ [ŷ] (예측)
[x₃] ──┤ ├──→ [h₅] ──┘
├──→ [h₃] ──┘
[x₄] ──┘
← ─ ─ ─ 가중치(w)로 연결 ─ ─ ─ →
뉴런 계산: output = activation(w₁x₁ + w₂x₂ + ... + b)
12.3 활성화 함수
활성화 함수는 뉴런의 출력에 비선형성을 부여합니다. 활성화 함수가 없으면 아무리 층을 깊게 쌓아도 결국 하나의 선형 변환에 불과합니다. 비선형 활성화 함수가 있어야 비로소 복잡한 패턴을 학습할 수 있습니다. "층을 깊게 쌓는 의미"를 부여하는 것이 활성화 함수의 역할입니다.
| 활성화 함수 | 수식 | 출력 범위 | 주 사용처 | 특징 |
|---|---|---|---|---|
| ReLU | max(0, x) | 0 ~ ∞ | 은닉층 (가장 보편적) | 계산 빠름, 기울기 소실 완화 |
| Sigmoid | 1 / (1 + e⁻ˣ) | 0 ~ 1 | 이진 분류 출력층 | 확률 해석 가능 |
| Softmax | eˣⁱ / Σeˣʲ | 0 ~ 1 (합=1) | 다중 분류 출력층 | 각 클래스 확률 출력 |
| Tanh | (eˣ - e⁻ˣ) / (eˣ + e⁻ˣ) | -1 ~ 1 | 은닉층 (RNN 등) | 0 중심, Sigmoid보다 나음 |
| Leaky ReLU | max(αx, x), α=0.01 | -∞ ~ ∞ | 은닉층 | ReLU의 "죽는 뉴런" 문제 해결 |
12.4 역전파 (Backpropagation)
역전파는 신경망의 핵심 학습 알고리즘입니다. 순전파로 예측값을 구하고, 손실 함수로 오차를 계산한 뒤, 이 오차를 출력층 → 은닉층 → 입력층 방향으로 거꾸로 전파하면서 각 가중치가 오차에 얼마나 기여했는지(그래디언트)를 계산합니다. 이때 미적분의 연쇄법칙(Chain Rule)이 사용됩니다. 그런 다음 경사 하강법으로 가중치를 업데이트합니다.
이 과정을 "순전파(예측) → 손실 계산 → 역전파(그래디언트 계산) → 가중치 업데이트" 순으로 수천~수만 번 반복하면 모델이 점차 정확해집니다. 전체 데이터를 한 번 순회하는 것을 에포크(Epoch)라고 하며, 보통 수십~수백 에포크를 학습합니다.
┌──────────────────────────────────────────────────────────┐ │ │ │ ① 순전파: X → [은닉층1] → [은닉층2] → ŷ (예측값) │ │ │ │ ② 손실 계산: Loss = f(ŷ, y) │ │ │ │ ③ 역전파: ∂Loss/∂w 를 출력→입력 방향으로 계산 │ │ (연쇄법칙으로 각 가중치의 기여도 파악) │ │ │ │ ④ 가중치 업데이트: w = w - α × ∂Loss/∂w │ │ (α = 학습률, Learning Rate) │ │ │ │ ⑤ ①~④를 에포크(Epoch)만큼 반복 │ │ │ └──────────────────────────────────────────────────────────┘
12.5 주요 최적화 알고리즘 (Optimizer)
경사 하강법에도 여러 변형이 있습니다. 가장 기본인 SGD(확률적 경사 하강법)는 미니배치 단위로 그래디언트를 계산하여 업데이트합니다. Momentum은 SGD에 관성을 추가하여 진동을 줄이고 수렴을 빠르게 합니다. Adam(Adaptive Moment Estimation)은 학습률을 파라미터별로 자동 조절하는 방법으로, 현재 가장 널리 사용되는 최적화 알고리즘입니다. 실무에서는 특별한 이유가 없다면 Adam을 기본으로 사용합니다.
from sklearn.neural_network import MLPClassifier
# scikit-learn의 다층 퍼셉트론 (간편 버전)
mlp = MLPClassifier(
hidden_layer_sizes=(128, 64), # 은닉층: 128개 → 64개 뉴런
activation='relu', # 활성화 함수
solver='adam', # 최적화 알고리즘
max_iter=500, # 최대 에포크
early_stopping=True, # 조기 종료 (과적합 방지)
validation_fraction=0.1, # 검증 데이터 10%
random_state=42
)
mlp.fit(X_train_scaled, y_train)
print(f"MLP 정확도: {mlp.score(X_test_scaled, y_test):.4f}")
# 학습 곡선 확인
import matplotlib.pyplot as plt
plt.plot(mlp.loss_curve_, label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('MLP Learning Curve')
plt.legend()
plt.show()
12.6 딥러닝 프레임워크
scikit-learn의 MLPClassifier는 간단한 실험에는 충분하지만, 실무에서 본격적인 딥러닝을 하려면 전용 프레임워크가 필요합니다. PyTorch(Meta)와 TensorFlow/Keras(Google)가 양대 산맥이며, 최근에는 연구와 실무 모두에서 PyTorch의 점유율이 크게 증가했습니다.
import torch
import torch.nn as nn
# PyTorch로 간단한 분류 신경망 정의
class SimpleNet(nn.Module):
def __init__(self, input_dim, num_classes):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Dropout(0.3), # 과적합 방지
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, num_classes)
)
def forward(self, x):
return self.network(x)
# 모델 생성
model = SimpleNet(input_dim=10, num_classes=2)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
12.7 딥러닝이 강력한 분야와 한계
딥러닝은 이미지 인식(CNN - 합성곱 신경망), 자연어 처리(Transformer, BERT, GPT), 음성 인식(RNN, Transformer), 생성 AI(GAN, Diffusion Model, LLM) 등에서 전통 머신러닝을 압도하는 성능을 보입니다. 하지만 정형 데이터(테이블 형태)에서는 여전히 XGBoost, LightGBM 같은 트리 기반 앙상블이 더 좋은 성능을 보이는 경우가 많습니다.
딥러닝의 한계도 명확합니다. 대량의 데이터와 강력한 GPU가 필요하며, 학습 시간이 오래 걸리고, 모델 내부가 블랙박스여서 해석이 어렵습니다. 또한 하이퍼파라미터가 매우 많아 튜닝에 많은 경험이 필요합니다.
엑셀 같은 테이블 형태의 정형 데이터에서는 XGBoost / LightGBM이 최강입니다.
이미지·텍스트·음성 같은 비정형 데이터에서는 딥러닝이 압도적입니다.
이것은 머신러닝에서 매우 중요한 경험 법칙이므로 반드시 기억하세요.
과적합(Overfitting)과 정규화(Regularization)
13.1 편향-분산 트레이드오프
모든 지도학습 모델의 예측 오차는 세 가지 요소로 분해됩니다. 편향(Bias), 분산(Variance), 그리고 줄일 수 없는 노이즈입니다. 이 세 요소의 관계를 이해하는 것이 머신러닝의 핵심 직관이며, 좋은 모델을 만드는 근본 원리입니다.
편향과 분산은 줄일 수 있지만, 노이즈는 데이터 자체의 불확실성으로 줄일 수 없음
편향(Bias)은 모델이 현실을 과도하게 단순화하여 발생하는 체계적 오차입니다. 편향이 높으면 과소적합(Underfitting)이 발생합니다. 예를 들어, 복잡한 곡선 패턴의 데이터에 직선을 맞추려고 하면 아무리 학습해도 패턴을 잡아낼 수 없습니다. "모델의 표현력이 부족한 상태"입니다.
분산(Variance)은 학습 데이터가 바뀔 때 모델의 예측이 얼마나 흔들리는지를 나타냅니다. 분산이 높으면 과적합(Overfitting)이 발생합니다. 학습 데이터의 노이즈까지 외워버려서, 학습 데이터에서는 완벽하지만 새로운 데이터에서는 성능이 급격히 떨어집니다. "모델이 너무 민감한 상태"입니다.
문제는 편향을 줄이면 분산이 증가하고, 분산을 줄이면 편향이 증가한다는 점입니다. 이것이 편향-분산 트레이드오프(Bias-Variance Tradeoff)입니다. 좋은 모델은 이 둘 사이의 최적 균형점을 찾는 것입니다.
| 상태 | 학습 성능 | 테스트 성능 | 편향 | 분산 | 비유 |
|---|---|---|---|---|---|
| 과소적합 | 낮음 | 낮음 | 높음 | 낮음 | 공부를 안 한 학생 |
| 적절한 적합 | 높음 | 높음 (비슷) | 낮음 | 낮음 | 개념을 이해한 학생 |
| 과적합 | 매우 높음 | 낮음 (큰 차이) | 낮음 | 높음 | 답을 외운 학생 |
13.2 과적합을 감지하는 방법
가장 간단한 방법은 학습 데이터와 테스트 데이터의 성능을 비교하는 것입니다. 학습 정확도는 99%인데 테스트 정확도가 75%라면 과적합을 강력히 의심해야 합니다. 이 갭(차이)이 클수록 과적합이 심한 것입니다.
학습 곡선(Learning Curve)을 그려보면 시각적으로 더 명확하게 확인할 수 있습니다. 학습 데이터 크기를 늘려가면서 학습/검증 성능을 그래프로 그리면, 과적합인 경우 두 곡선의 간격이 벌어지는 것을 관찰할 수 있습니다.
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt
# 학습 곡선으로 과적합 진단
train_sizes, train_scores, val_scores = learning_curve(
model, X_train, y_train,
train_sizes=np.linspace(0.1, 1.0, 10),
cv=5, scoring='accuracy'
)
plt.figure(figsize=(8, 5))
plt.plot(train_sizes, train_scores.mean(axis=1), label='Train')
plt.plot(train_sizes, val_scores.mean(axis=1), label='Validation')
plt.xlabel('Training Set Size')
plt.ylabel('Accuracy')
plt.title('Learning Curve - 과적합 진단')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# 두 곡선의 간격이 크면 → 과적합
# 두 곡선 모두 낮으면 → 과소적합
# 두 곡선이 높고 가까우면 → 적절한 적합 ✓
13.3 정규화 기법들
정규화(Regularization)는 모델의 복잡도에 페널티를 부여하여 과적합을 방지하는 기법입니다. 손실 함수에 가중치 크기에 대한 페널티 항을 추가하여, 모델이 불필요하게 큰 가중치를 가지지 않도록 제약합니다.
L1 정규화 (Lasso)
가중치의 절대값 합(|w₁| + |w₂| + ... + |wₙ|)에 페널티를 부여합니다. L1의 핵심 특징은 일부 가중치를 정확히 0으로 만든다는 점입니다. 이는 자동으로 불필요한 특성을 제거하는 특성 선택(Feature Selection) 효과가 있습니다. 특성이 수백~수천 개이고 그 중 소수만 중요한 경우에 특히 유용합니다.
L2 정규화 (Ridge)
가중치의 제곱합(w₁² + w₂² + ... + wₙ²)에 페널티를 부여합니다. 모든 가중치를 0에 가깝게 축소하지만 완전히 0으로 만들지는 않습니다. 모든 특성을 유지하면서 각 특성의 영향력을 적절히 조절하고 싶을 때 적합합니다. 다중공선성(특성 간 상관이 높은 경우) 문제도 완화합니다.
ElasticNet
L1과 L2를 혼합한 정규화입니다. 두 기법의 장점을 동시에 취할 수 있으며, 혼합 비율(l1_ratio)을 조절할 수 있습니다. l1_ratio=1이면 순수 Lasso, l1_ratio=0이면 순수 Ridge가 됩니다.
Lasso: Loss = MSE + α × Σ|wᵢ|
ElasticNet: Loss = MSE + α₁ × Σ|wᵢ| + α₂ × Σ(wᵢ²)
α (alpha): 정규화 강도. 클수록 가중치를 더 강하게 제약
from sklearn.linear_model import Ridge, Lasso, ElasticNet
# Ridge (L2 정규화)
ridge = Ridge(alpha=1.0) # alpha: 정규화 강도
ridge.fit(X_train, y_train)
print(f"Ridge R²: {ridge.score(X_test, y_test):.4f}")
# Lasso (L1 정규화)
lasso = Lasso(alpha=0.01)
lasso.fit(X_train, y_train)
print(f"Lasso R²: {lasso.score(X_test, y_test):.4f}")
# 0이 된 가중치 개수 확인 (Lasso의 특성 선택 효과)
print(f"전체 특성 수: {len(lasso.coef_)}")
print(f"Lasso가 제거한 특성 수: {(lasso.coef_ == 0).sum()}")
print(f"살아남은 특성 수: {(lasso.coef_ != 0).sum()}")
# ElasticNet (L1 + L2 혼합)
enet = ElasticNet(alpha=0.01, l1_ratio=0.5)
enet.fit(X_train, y_train)
print(f"ElasticNet R²: {enet.score(X_test, y_test):.4f}")
# alpha 값에 따른 성능 변화 실험
for alpha in [0.001, 0.01, 0.1, 1.0, 10.0]:
ridge = Ridge(alpha=alpha)
ridge.fit(X_train, y_train)
print(f" alpha={alpha:6.3f} → R²={ridge.score(X_test, y_test):.4f}")
13.4 과적합 방지를 위한 기타 전략
정규화 외에도 과적합을 방지하는 여러 전략이 있습니다.
더 많은 데이터 수집이 가장 근본적이고 효과적인 해결책입니다. 데이터가 많을수록 모델이 노이즈 대신 진짜 패턴을 학습할 가능성이 높아집니다. 데이터 증강(Data Augmentation)도 같은 맥락입니다.
특성 선택(Feature Selection)으로 불필요한 특성을 제거하면 모델이 관련 없는 패턴을 학습하는 것을 방지합니다. 특성 중요도, 상관분석, Lasso를 활용할 수 있습니다.
트리 기반 모델의 가지치기(Pruning)로 max_depth, min_samples_split, min_samples_leaf 등을 제한하면 트리가 과도하게 성장하는 것을 막습니다.
신경망의 드롭아웃(Dropout)은 학습 시 매번 뉴런의 일부를 랜덤하게 비활성화하여, 특정 뉴런에 의존하지 않는 강건한 모델을 만듭니다. 조기 종료(Early Stopping)는 검증 데이터의 성능이 더 이상 개선되지 않으면 학습을 중단합니다.
모델 평가 지표 (Evaluation Metrics)
14.1 왜 다양한 평가 지표가 필요한가?
모델의 성능을 하나의 숫자로 완벽히 표현할 수는 없습니다. 특히 분류 문제에서 "정확도(Accuracy) 95%"라는 수치가 항상 좋은 것은 아닙니다. 1000건의 데이터 중 950건이 정상이고 50건만 사기인 상황을 생각해보세요. "모두 정상"이라고 예측하는 바보 모델도 정확도 95%를 달성합니다. 하지만 이 모델은 사기를 하나도 못 잡으므로 완전히 쓸모없습니다.
이처럼 클래스 불균형(Class Imbalance)이 있을 때 정확도는 매우 기만적인 지표가 됩니다. 목적에 맞는 적절한 평가 지표를 선택하는 것이 좋은 모델을 만드는 핵심입니다.
14.2 혼동 행렬 (Confusion Matrix)
혼동 행렬은 모든 분류 평가 지표의 기초가 되는 2×2 표입니다. 모델이 어디에서 맞히고 어디에서 틀리는지를 한눈에 보여줍니다.
예측값
양성(+) 음성(-)
실제 양성(+) TP FN
음성(-) FP TN
TP (True Positive) : 양성을 양성으로 올바르게 예측 ✓ (적중)
TN (True Negative) : 음성을 음성으로 올바르게 예측 ✓ (올바른 기각)
FP (False Positive) : 음성을 양성으로 잘못 예측 ✗ (오탐, 1종 오류)
FN (False Negative) : 양성을 음성으로 잘못 예측 ✗ (미탐, 2종 오류)
예) 암 진단: FN(암 환자를 정상 판정) → 치명적!
예) 스팸 필터: FP(정상 메일을 스팸 처리) → 매우 불편!
14.3 분류 핵심 평가 지표
| 지표 | 수식 | 의미 | 중요한 상황 |
|---|---|---|---|
| 정확도 (Accuracy) | (TP+TN) / 전체 | 전체 중 올바른 예측 비율 | 클래스가 균형일 때만 |
| 정밀도 (Precision) | TP / (TP+FP) | "양성 예측" 중 실제 양성 비율 | 오탐(FP)이 비용이 클 때 |
| 재현율 (Recall) | TP / (TP+FN) | "실제 양성" 중 올바르게 탐지한 비율 | 미탐(FN)이 위험할 때 |
| F1-Score | 2 × (P×R) / (P+R) | 정밀도와 재현율의 조화 평균 | P와 R의 균형이 필요할 때 |
| AUC-ROC | ROC 곡선 아래 면적 | 임계값에 무관한 종합 분류 능력 | 전체적 모델 비교 |
정밀도 vs 재현율: 어떤 것을 우선할 것인가?
이 둘은 트레이드오프 관계에 있습니다. 분류 임계값(threshold)을 낮추면 더 많이 양성으로 예측하므로 재현율은 올라가지만 정밀도는 내려가고, 임계값을 높이면 반대가 됩니다.
정밀도 우선이 적합한 경우: 스팸 필터(정상 메일이 스팸으로 처리되면 안 됨), 추천 시스템(엉뚱한 추천은 사용자 이탈을 유발), 범죄 수사(무고한 사람을 범인으로 지목하면 안 됨)
재현율 우선이 적합한 경우: 암 진단(암 환자를 놓치면 생명 위험), 사기 탐지(사기를 놓치면 금전적 손실), 제조업 불량 검출(불량품이 출시되면 안 됨)
from sklearn.metrics import (
confusion_matrix, classification_report,
roc_auc_score, roc_curve, precision_recall_curve
)
import matplotlib.pyplot as plt
# 혼동 행렬
cm = confusion_matrix(y_test, y_pred)
print("혼동 행렬:")
print(cm)
# 종합 분류 보고서 (Precision, Recall, F1 한번에)
print("\n분류 보고서:")
print(classification_report(y_test, y_pred,
target_names=['음성', '양성']))
# AUC-ROC 곡선 시각화
y_prob = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_prob)
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='#ff6b35',
label=f'Model (AUC = {auc:.4f})', linewidth=2)
plt.plot([0, 1], [0, 1], 'k--', label='Random (AUC = 0.5)', alpha=0.5)
plt.xlabel('False Positive Rate (1 - 특이도)')
plt.ylabel('True Positive Rate (재현율)')
plt.title('ROC Curve')
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)
plt.show()
14.4 회귀 평가 지표
| 지표 | 수식 | 범위 | 특징 |
|---|---|---|---|
| MSE | (1/n) × Σ(y-ŷ)² | 0 ~ ∞ (낮을수록 좋음) | 큰 오차에 민감, 미분 가능(학습에 유리) |
| RMSE | √MSE | 0 ~ ∞ (낮을수록 좋음) | 원래 단위로 해석 가능 (가장 보편적) |
| MAE | (1/n) × Σ|y-ŷ| | 0 ~ ∞ (낮을수록 좋음) | 이상치에 강건, 직관적 |
| R² Score | 1 - (SS_res / SS_tot) | -∞ ~ 1 (1에 가까울수록 좋음) | 모델의 설명력 비율 |
| MAPE | (1/n) × Σ|y-ŷ|/|y| × 100 | 0 ~ ∞ (낮을수록 좋음) | 백분율 오차, 스케일 무관 비교 가능 |
from sklearn.metrics import (
mean_squared_error, mean_absolute_error, r2_score
)
import numpy as np
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"MSE : {mse:,.2f}")
print(f"RMSE : {rmse:,.2f}")
print(f"MAE : {mae:,.2f}")
print(f"R² : {r2:.4f}")
• 클래스 균형 + 분류 → Accuracy로 충분
• 클래스 불균형 + 분류 → F1, AUC-ROC 필수
• 오탐이 중요 → Precision 우선
• 미탐이 위험 → Recall 우선
• 회귀 → RMSE(기본) + R²(해석용)
교차 검증 (Cross-Validation)
15.1 왜 단순 분할로는 부족한가?
데이터를 한 번만 학습/테스트로 나누면, 어떤 데이터가 학습에 들어가고 어떤 데이터가 테스트에 들어가느냐에 따라 성능이 크게 달라질 수 있습니다. 운 좋게 쉬운 테스트 셋을 뽑으면 성능이 과대평가되고, 어려운 셋을 뽑으면 과소평가됩니다.
더 심각한 문제는, 이 하나의 분할 결과만 보고 모델을 선택하면 특정 테스트 셋에 과적합(overfitting to the test set)될 수 있다는 점입니다. 교차 검증은 이런 운의 요소를 제거하고 모델의 "진짜 실력"을 신뢰성 있게 추정하기 위한 기법입니다.
15.2 K-Fold 교차 검증
K-Fold 교차 검증은 전체 데이터를 K개의 균등한 조각(Fold)으로 나눕니다. K번 반복하면서 매번 다른 Fold를 검증(테스트) 셋으로, 나머지 K-1개 Fold를 학습 셋으로 사용합니다. K번의 성능을 평균 내면 단일 분할보다 훨씬 안정적인 성능 추정치를 얻을 수 있으며, 표준편차를 통해 모델의 안정성도 파악할 수 있습니다.
데이터를 5등분: [A] [B] [C] [D] [E]
반복 1: [검증] [학습] [학습] [학습] [학습] → Score₁ = 0.84
반복 2: [학습] [검증] [학습] [학습] [학습] → Score₂ = 0.87
반복 3: [학습] [학습] [검증] [학습] [학습] → Score₃ = 0.82
반복 4: [학습] [학습] [학습] [검증] [학습] → Score₄ = 0.86
반복 5: [학습] [학습] [학습] [학습] [검증] → Score₅ = 0.85
최종 성능 = (0.84 + 0.87 + 0.82 + 0.86 + 0.85) / 5
= 0.848 ± 0.017
→ 평균이 높고 표준편차가 작으면 안정적이고 좋은 모델!
from sklearn.model_selection import cross_val_score, StratifiedKFold
# 가장 간단한 교차 검증
scores = cross_val_score(
model, X, y,
cv=5, # 5-Fold
scoring='accuracy' # 평가 지표
)
print(f"평균 정확도: {scores.mean():.4f} (±{scores.std():.4f})")
print(f"각 Fold 점수: {scores}")
# 분류: Stratified K-Fold (클래스 비율 유지) ★추천
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skf, scoring='f1')
print(f"평균 F1: {scores.mean():.4f} (±{scores.std():.4f})")
# 여러 지표를 동시에 평가
from sklearn.model_selection import cross_validate
results = cross_validate(
model, X, y, cv=5,
scoring=['accuracy', 'f1', 'roc_auc']
)
for metric in ['test_accuracy', 'test_f1', 'test_roc_auc']:
vals = results[metric]
print(f"{metric:18s}: {vals.mean():.4f} (±{vals.std():.4f})")
15.3 교차 검증의 변형들
Stratified K-Fold
분류 문제에서 각 Fold의 클래스 비율을 전체 데이터와 동일하게 유지하는 방식입니다. 불균형 데이터에서 필수적이며, 사실상 분류 문제에서는 항상 Stratified를 쓰는 것이 좋습니다. scikit-learn의 cross_val_score에 분류 모델을 넣으면 자동으로 Stratified가 적용됩니다.
Leave-One-Out (LOO)
데이터가 매우 적을 때(수십 건) 사용합니다. 한 건만 테스트로 빼고 나머지 N-1건으로 학습하는 것을 N번 반복합니다. 가장 편향 없는 추정이지만 계산 비용이 큽니다.
Time Series Split
시계열 데이터에서는 미래 데이터를 학습에 쓰면 안 됩니다. Time Series Split은 항상 과거 데이터로 학습하고 미래 데이터로 테스트하는 방식으로, 시간 순서를 보장합니다.
from sklearn.model_selection import (
LeaveOneOut, TimeSeriesSplit, RepeatedStratifiedKFold
)
# 시계열 교차 검증
tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(model, X, y, cv=tscv, scoring='neg_mean_squared_error')
# 반복 교차 검증 (더 안정적인 추정)
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42)
scores = cross_val_score(model, X, y, cv=rskf, scoring='f1')
print(f"반복 CV (5-Fold × 3회 = 15회): {scores.mean():.4f} (±{scores.std():.4f})")
하이퍼파라미터 튜닝
16.1 파라미터 vs 하이퍼파라미터
파라미터(Parameter)는 모델이 학습을 통해 자동으로 결정하는 내부 값입니다. 선형 회귀의 가중치(w)와 편향(b), 신경망의 연결 가중치 등이 파라미터입니다. 이것은 우리가 건드릴 필요 없이 알고리즘이 데이터를 통해 알아서 찾아줍니다.
하이퍼파라미터(Hyperparameter)는 사용자가 학습 전에 미리 설정해야 하는 외부 값입니다. 랜덤 포레스트의 트리 수(n_estimators), 결정 트리의 최대 깊이(max_depth), 경사 하강법의 학습률(learning_rate), 정규화 강도(alpha) 등이 하이퍼파라미터입니다. 이 값들은 모델의 학습 방식과 복잡도를 결정하며, 성능에 큰 영향을 미칩니다.
| 구분 | 파라미터 | 하이퍼파라미터 |
|---|---|---|
| 결정 주체 | 모델이 자동으로 학습 | 사용자가 사전에 설정 |
| 예시 | 가중치, 편향, 분할 기준 | 학습률, 트리 수, 깊이, alpha |
| 최적화 방법 | 경사 하강법 등 학습 알고리즘 | Grid Search, Random Search 등 |
16.2 Grid Search (격자 탐색)
시도할 하이퍼파라미터 값의 목록을 정의하고, 모든 조합을 빠짐없이 시도하는 방법입니다. 가장 확실하지만, 조합이 많으면 시간이 기하급수적으로 늘어납니다. 파라미터 3개 × 각 4개 값 = 64가지 조합, 여기에 5-Fold 교차 검증이면 총 320번 학습합니다.
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
# 탐색할 하이퍼파라미터 격자 정의
param_grid = {
'n_estimators': [100, 200, 300],
'max_depth': [5, 10, 15, None],
'min_samples_split': [2, 5, 10]
}
# 총 조합: 3 × 4 × 3 = 36가지 × 5-Fold = 180번 학습
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5, # 5-Fold 교차 검증
scoring='f1', # 최적화할 지표
n_jobs=-1, # 모든 CPU 코어 사용 (병렬 처리)
verbose=1 # 진행 상황 출력
)
grid_search.fit(X_train, y_train)
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최고 CV F1: {grid_search.best_score_:.4f}")
# 최적 모델로 테스트 셋 평가
best_model = grid_search.best_estimator_
print(f"테스트 정확도: {best_model.score(X_test, y_test):.4f}")
16.3 Random Search (랜덤 탐색)
모든 조합을 시도하는 대신, 정해진 분포에서 랜덤하게 일부 조합만 샘플링하여 시도합니다. 직관적으로 Grid Search보다 불리해 보이지만, 실제로는 더 효율적인 경우가 많습니다. 이유는 간단합니다. 대부분의 모델에서 모든 하이퍼파라미터가 동일하게 중요한 것이 아니며, 성능에 결정적인 영향을 미치는 하이퍼파라미터는 1~2개인 경우가 대부분입니다. 랜덤 탐색은 중요한 하이퍼파라미터의 더 다양한 값을 시도할 수 있습니다.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
# 연속적인 분포에서 샘플링 가능!
param_dist = {
'n_estimators': randint(50, 500), # 50~500 정수 중 랜덤
'max_depth': randint(3, 30), # 3~30 정수 중 랜덤
'learning_rate': uniform(0.01, 0.29), # 0.01~0.30 실수 균등분포
'min_samples_split': randint(2, 20),
'subsample': uniform(0.6, 0.4) # 0.6~1.0
}
random_search = RandomizedSearchCV(
GradientBoostingClassifier(random_state=42),
param_dist,
n_iter=100, # 100번만 랜덤 시도
cv=5,
scoring='f1',
random_state=42,
n_jobs=-1,
verbose=1
)
random_search.fit(X_train, y_train)
print(f"최적 파라미터: {random_search.best_params_}")
print(f"최고 CV F1: {random_search.best_score_:.4f}")
16.4 Bayesian Optimization (베이지안 최적화)
가장 진보된 튜닝 방법입니다. 이전 탐색 결과를 학습하여 "어디를 다음에 탐색하면 가장 유망할지"를 지능적으로 결정합니다. Grid Search처럼 무작정 전수조사하거나, Random Search처럼 무작위로 찍는 것이 아니라, 과거 경험을 활용하여 효율적으로 최적점을 찾아갑니다. Optuna 라이브러리가 가장 널리 사용됩니다.
import optuna
from sklearn.model_selection import cross_val_score
def objective(trial):
# Optuna가 지능적으로 값을 제안
params = {
'n_estimators': trial.suggest_int('n_estimators', 50, 500),
'max_depth': trial.suggest_int('max_depth', 3, 15),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'min_samples_split': trial.suggest_int('min_samples_split', 2, 20),
}
model = GradientBoostingClassifier(**params, random_state=42)
score = cross_val_score(model, X_train, y_train, cv=5, scoring='f1')
return score.mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100, show_progress_bar=True)
print(f"최적 파라미터: {study.best_params}")
print(f"최고 F1: {study.best_value:.4f}")
16.5 튜닝 전략 비교 요약
| 방법 | 원리 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|---|
| Grid Search | 모든 격자점 탐색 | 최적값 보장 (범위 내) | 조합 폭발, 매우 느림 | 파라미터 2~3개, 값 범위 좁을 때 |
| Random Search | 랜덤 샘플링 | 효율적, 넓은 범위 탐색 | 최적값 미보장 | 초기 탐색, 파라미터 많을 때 |
| Bayesian (Optuna) | 이전 결과로 학습하여 탐색 | 가장 효율적, 지능적 | 추가 라이브러리 필요 | 대규모 프로젝트, 최종 튜닝 |
1단계: 기본값으로 여러 모델 빠르게 비교 → 유망한 모델 1~2개 선정
2단계: Random Search로 넓은 범위를 빠르게 탐색 → 유망한 범위 파악
3단계: 유망한 범위에서 Grid Search 또는 Optuna로 정밀 탐색
실전 프로젝트 - 타이타닉 생존자 예측
지금까지 배운 모든 내용을 종합하여 실전 프로젝트를 진행합니다. 유명한 타이타닉 데이터셋으로 승객의 생존 여부를 예측하는 이진 분류 문제를 풀어보겠습니다. 전처리 → 모델 비교 → 튜닝 → 평가 → 해석의 전체 워크플로를 경험합니다.
17.1 데이터 로드 및 탐색 (EDA)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, roc_auc_score
# 데이터 로드 (seaborn 내장 데이터)
df = sns.load_dataset('titanic')
print(f"데이터 크기: {df.shape}") # (891, 15)
print(f"\n결측값:\n{df.isnull().sum()}")
# 타겟 변수 분포 확인
print(f"\n생존율:\n{df['survived'].value_counts(normalize=True)}")
# 0 (사망): 61.6% | 1 (생존): 38.4%
# 주요 특성별 생존율 탐색
print(f"\n성별 생존율:\n{df.groupby('sex')['survived'].mean()}")
# female: 74.2% | male: 18.9%
print(f"\n좌석 등급별 생존율:\n{df.groupby('pclass')['survived'].mean()}")
# 1등석: 63.0% | 2등석: 47.3% | 3등석: 24.2%
17.2 데이터 전처리
# 사용할 특성 선택
features = ['pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked']
target = 'survived'
df_ml = df[features + [target]].copy()
# ── 결측값 처리 ──
# age: 177건 결측 → 중앙값으로 채우기 (이상치에 강건)
df_ml['age'].fillna(df_ml['age'].median(), inplace=True)
# embarked: 2건 결측 → 최빈값으로 채우기
df_ml['embarked'].fillna(df_ml['embarked'].mode()[0], inplace=True)
# ── 특성 공학 (Feature Engineering) ──
# 가족 크기 = 형제/배우자 + 부모/자녀 + 본인
df_ml['family_size'] = df_ml['sibsp'] + df_ml['parch'] + 1
# 혼자 탑승 여부
df_ml['is_alone'] = (df_ml['family_size'] == 1).astype(int)
# ── 범주형 인코딩 ──
df_ml = pd.get_dummies(df_ml, columns=['sex', 'embarked'], drop_first=True)
# ── 특성/타겟 분리 ──
X = df_ml.drop(target, axis=1)
y = df_ml[target]
print(f"특성: {list(X.columns)}")
print(f"데이터 크기: X={X.shape}, y={y.shape}")
# ── 데이터 분할 ──
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# ── 스케일링 (SVM, KNN용) ──
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
17.3 여러 모델 비교
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import (
RandomForestClassifier, GradientBoostingClassifier
)
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
# 비교할 모델 딕셔너리
models = {
'Logistic Regression': (LogisticRegression(max_iter=1000), False),
'Decision Tree': (DecisionTreeClassifier(max_depth=5), False),
'Random Forest': (RandomForestClassifier(n_estimators=200), False),
'Gradient Boosting': (GradientBoostingClassifier(n_estimators=200), False),
'SVM (RBF)': (SVC(kernel='rbf', probability=True), True),
'KNN (k=7)': (KNeighborsClassifier(n_neighbors=7), True),
}
print(f"{'모델':<25s} {'CV 평균':>8s} {'CV 표준편차':>10s}")
print("─" * 50)
results = {}
for name, (model, needs_scaling) in models.items():
X_cv = X_train_scaled if needs_scaling else X_train
scores = cross_val_score(model, X_cv, y_train, cv=5, scoring='accuracy')
results[name] = scores
print(f"{name:<25s} {scores.mean():8.4f} {scores.std():10.4f}")
17.4 최적 모델 튜닝 및 최종 평가
from sklearn.model_selection import GridSearchCV
# Gradient Boosting이 가장 좋았으므로 정밀 튜닝
param_grid = {
'n_estimators': [100, 200, 300],
'max_depth': [3, 4, 5],
'learning_rate': [0.01, 0.05, 0.1],
'min_samples_split': [2, 5]
}
grid = GridSearchCV(
GradientBoostingClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1,
verbose=1
)
grid.fit(X_train, y_train)
print(f"최적 파라미터: {grid.best_params_}")
print(f"최적 CV 점수: {grid.best_score_:.4f}")
# ── 최종 테스트 평가 ──
best = grid.best_estimator_
y_pred = best.predict(X_test)
y_prob = best.predict_proba(X_test)[:, 1]
print("\n══════ 최종 테스트 결과 ══════")
print(classification_report(y_test, y_pred,
target_names=['사망', '생존']))
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")
17.5 특성 중요도 분석 및 인사이트
# 어떤 특성이 생존에 가장 큰 영향을 미쳤는가?
importance = pd.Series(
best.feature_importances_,
index=X.columns
).sort_values(ascending=True)
plt.figure(figsize=(9, 6))
importance.plot.barh(color='#ff6b35', edgecolor='#333')
plt.title('Feature Importance - 타이타닉 생존 예측', fontsize=14)
plt.xlabel('Importance')
plt.tight_layout()
plt.show()
# ── 인사이트 ──
# 1. sex_male (성별): 가장 큰 영향. 여성이 남성보다 생존율 훨씬 높음
# 2. fare (운임): 높은 운임 = 높은 등급 = 높은 생존율
# 3. age (나이): 어린아이 우선 구조 정책 반영
# 4. pclass (좌석 등급): 1등석 >> 2등석 >> 3등석 생존율
# → "여성과 아이 우선(Women and Children First)" 정책이 데이터로 증명됨
① EDA: 데이터를 먼저 탐색하여 패턴과 문제점을 파악합니다.
② 전처리: 결측값 처리, 인코딩, 특성 공학을 수행합니다.
③ 베이스라인: 여러 모델을 기본값으로 빠르게 비교합니다.
④ 튜닝: 유망한 모델의 하이퍼파라미터를 최적화합니다.
⑤ 평가: 테스트 셋으로 최종 성능을 검증합니다.
⑥ 해석: 특성 중요도로 비즈니스 인사이트를 도출합니다.
이 흐름은 실무의 모든 머신러닝 프로젝트에 그대로 적용됩니다.
부록 - 알고리즘 치트시트 & 학습 로드맵
18.1 지도학습 알고리즘 한눈에 비교
| 알고리즘 | 유형 | 스케일링 | 해석성 | 학습 속도 | 추천 상황 |
|---|---|---|---|---|---|
| 선형 회귀 | 회귀 | 권장 | ★★★★★ | 매우 빠름 | 선형 관계, 해석 중요 |
| 로지스틱 회귀 | 분류 | 권장 | ★★★★★ | 매우 빠름 | 이진 분류, 확률 필요 |
| 결정 트리 | 둘 다 | 불필요 | ★★★★☆ | 빠름 | 해석 필요, 비선형 |
| 랜덤 포레스트 | 둘 다 | 불필요 | ★★★☆☆ | 보통 | 범용적, 안정적 |
| XGBoost | 둘 다 | 불필요 | ★★☆☆☆ | 보통 | 정형 데이터 최강 |
| LightGBM | 둘 다 | 불필요 | ★★☆☆☆ | 빠름 | 대용량 정형 데이터 |
| CatBoost | 둘 다 | 불필요 | ★★☆☆☆ | 보통 | 범주형 특성 많을 때 |
| SVM | 둘 다 | 필수 | ★★☆☆☆ | 느림 | 고차원, 소량 데이터 |
| KNN | 둘 다 | 필수 | ★★★☆☆ | 예측 느림 | 소규모, 저차원 |
| 나이브 베이즈 | 분류 | 불필요 | ★★★★☆ | 매우 빠름 | 텍스트 분류, 소량 데이터 |
| 신경망 (MLP) | 둘 다 | 필수 | ★☆☆☆☆ | 느림 | 복잡한 비선형, 대량 데이터 |
18.2 문제 유형별 알고리즘 선택 가이드
데이터 유형?
/ \
정형 데이터 비정형 데이터
(테이블/CSV) (이미지/텍스트/음성)
| |
예측 유형? → 딥러닝
/ \ (CNN, Transformer, RNN)
회귀 분류
| |
데이터 크기? 클래스 균형?
/ \ / \
소량 대량 균형 불균형
| | | |
선형회귀 XGBoost 로지스틱 XGBoost
Ridge LightGBM Random + SMOTE
RF Forest + class_weight
★ 어떤 상황이든 첫 번째로 할 일:
간단한 모델(선형/로지스틱)로 베이스라인을 만드세요!
18.3 흔한 실수 모음
| 실수 | 결과 | 해결법 |
|---|---|---|
| 테스트 데이터로 fit_transform | 데이터 누수, 성능 과대평가 | train으로 fit, test는 transform만 |
| 불균형 데이터에서 Accuracy만 확인 | 성능 착각 (바보 모델도 95%) | F1, AUC-ROC, 혼동 행렬 활용 |
| 교차 검증 없이 단일 split만 | 불안정하고 편향된 평가 | 최소 5-Fold 교차 검증 사용 |
| 스케일링 없이 SVM/KNN 사용 | 성능 급격히 저하 | 반드시 StandardScaler 적용 |
| 과적합 확인 안 함 | 실전 배포 시 성능 급락 | 학습/테스트 성능 갭 모니터링 |
| 모든 특성을 무조건 사용 | 노이즈로 성능 저하 | 특성 중요도 분석, 특성 선택 |
| 하이퍼파라미터 기본값만 사용 | 잠재 성능 미발휘 | Grid/Random Search/Optuna 활용 |
| EDA 없이 바로 모델링 | 전처리 부실, 인사이트 놓침 | 데이터 탐색을 충분히 수행 |
| 범주형 인코딩에 라벨 인코딩 남용 | 순서 없는 범주에 순서 부여 | 순서 없으면 원-핫 인코딩 사용 |
18.4 핵심 Python 코드 치트시트
# =============================================
# 지도학습 치트시트 - 핵심 코드 한눈에 보기
# =============================================
# ── 1. 데이터 분할 ──
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
# ── 2. 전처리 ──
from sklearn.preprocessing import StandardScaler, MinMaxScaler
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test) # ★ transform만!
# ── 3. 모델 학습 (공통 패턴) ──
model.fit(X_train, y_train) # 학습
y_pred = model.predict(X_test) # 예측 (클래스)
y_prob = model.predict_proba(X_test) # 예측 (확률)
model.score(X_test, y_test) # 정확도
# ── 4. 주요 모델 import ──
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.svm import SVC, SVR
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier, XGBRegressor
from lightgbm import LGBMClassifier, LGBMRegressor
# ── 5. 분류 평가 지표 ──
from sklearn.metrics import (
accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, classification_report,
confusion_matrix
)
# ── 6. 회귀 평가 지표 ──
from sklearn.metrics import (
mean_squared_error, mean_absolute_error, r2_score
)
# ── 7. 교차 검증 ──
from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"{scores.mean():.4f} ± {scores.std():.4f}")
# ── 8. 하이퍼파라미터 튜닝 ──
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
grid = GridSearchCV(model, param_grid, cv=5, scoring='f1', n_jobs=-1)
grid.fit(X_train, y_train)
best = grid.best_estimator_
print(grid.best_params_, grid.best_score_)
# ── 9. 파이프라인 (전처리 + 모델 일체화) ──
from sklearn.pipeline import Pipeline
pipe = Pipeline([
('scaler', StandardScaler()),
('model', SVC(kernel='rbf'))
])
pipe.fit(X_train, y_train)
pipe.score(X_test, y_test)
# ── 10. 특성 중요도 (트리 기반 모델) ──
import pandas as pd
importance = pd.Series(model.feature_importances_, index=feature_names)
importance.sort_values(ascending=False).head(10)
18.5 학습 로드맵
| 단계 | 기간 | 학습 내용 | 이 튜토리얼 | 추천 자료 |
|---|---|---|---|---|
| Level 1 입문 | 1~2주 | ML 개요, 회귀/분류, 전처리 기초 | Ch.1 ~ Ch.4 | Coursera ML 강좌 |
| Level 2 기초 | 2~4주 | 주요 알고리즘 이해와 구현 | Ch.5 ~ Ch.12 | scikit-learn 공식 문서 |
| Level 3 중급 | 1~2개월 | 과적합, 평가 지표, 교차 검증, 튜닝 | Ch.13 ~ Ch.16 | Hands-On ML 도서 |
| Level 4 실전 | 2~3개월 | Kaggle 프로젝트, 특성 공학, 파이프라인 | Ch.17 + Kaggle | Kaggle Competitions |
| Level 5 심화 | 지속 | 딥러닝, NLP, 컴퓨터 비전, MLOps | 추가 학습 | PyTorch 공식 튜토리얼 |
18.6 마치며
축하합니다! 전 18장의 '지도학습의 모든 것' 튜토리얼을 모두 마치셨습니다.
지도학습은 머신러닝에서 가장 기본이 되면서도 가장 실용적인 분야입니다. 이 튜토리얼에서 다룬 알고리즘들은 수십 년간 검증된 강력한 도구이며, 비즈니스 현장에서 마주하는 대부분의 예측 문제를 해결할 수 있습니다. 선형 회귀부터 신경망까지, 과적합 방지부터 하이퍼파라미터 튜닝까지, 핵심 개념을 모두 익히셨습니다.
하지만 이론만으로는 충분하지 않습니다. 진정한 실력은 직접 데이터를 다루고, 실수하고, 디버깅하는 과정에서 쌓입니다. Kaggle에 참여하거나, 관심 있는 주제(주식, 스포츠, 건강, 영화 등)로 나만의 프로젝트를 시작하세요.
1. 데이터를 이해하세요 — 어떤 알고리즘보다 데이터의 품질과 전처리가 중요합니다. EDA에 전체 시간의 절반을 투자해도 아깝지 않습니다. "데이터를 이해한 사람이 좋은 모델을 만든다"는 것은 진리입니다.
2. 단순한 모델부터 시작하세요 — 복잡한 모델이 항상 좋은 것은 아닙니다. 선형 회귀나 로지스틱 회귀로 먼저 베이스라인을 만들고, 점진적으로 복잡도를 높여가세요. 단순한 모델이 의외로 좋은 성능을 내는 경우가 많습니다.
3. 직접 코드를 작성하세요 — 이론을 100번 읽는 것보다 코드를 1번 작성하는 것이 더 효과적입니다. 복사-붙여넣기가 아닌, 직접 타이핑하면서 손으로 기억하세요. 에러를 만나고 해결하는 과정에서 가장 많이 성장합니다.
데이터 사이언스의 세계에 오신 것을 환영합니다. 이 튜토리얼이 여러분의 머신러닝 여정에 든든한 첫걸음이 되기를 바랍니다!